import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedList;

//
//
//  @ Project : Scheduler
//  @ File Name : Scheduler.java
//  @ Date : 2/18/2010
//  @ Author : Ken Uemura, Tarun Morton
//

/** 
 * Scheduler is responsible for,
 * 	- Holding a Job Pool (jobList).
 *  - Inserting a Job to appropriate Queue based on
 *    its PriorityID.
 *  - Preempts Process in CPU when process with higher
 *    priority arrives.
 *  - Keeps Aging & Feedback informations.
 *  - Tracing.
 *  @author Morton & Uemura
 */
public class Scheduler {

	/** Feedback Priority Shift */
	private static final int FEEDBACK = 4;
	
	/** Aging Factor */
	private static final double AGING = 100.0;
	
	/** List of legal device names */
	private String [] deviceNames = { "Disk", "Pr1", "Pr2" };

	/** Trace output */
	private String traceStatement;
	
	/** Clock represented as string */
	private String clockStatement;
	
	/** Flag for tracing */
	private boolean trace;
	
	/** Flag for aging */
	private boolean aging;

	/** Control Arithmetic errors */
	private DecimalFormat df;
	
	/** Clock Counter */
	private double clockTime;
	
	/** Job Pool */
	private LinkedList <Process> jobList;
	
	/** Ready Queue */
	private ReadyQueue readyQueue;
	
	/** CPU */
	private CPU singleProcessor;
	
	/** Disk IO */
	private Device disk;
	
	/** Printer 1 IO */
	private Device printer1;
	
	/** Printer 2 IO */
	private Device printer2;
	
	/**
	 * Completed Processes.
	 */
	private ArrayList <Process> completed;

	/**
	   Constructs a Default Scheduler
	 */
	public Scheduler() 
	{
		this.trace 					= false;
		this.aging 					= false;
		this.clockTime 				= 0;
		this.df						= new DecimalFormat("0.0");
		this.jobList				= new LinkedList<Process>();
		this.readyQueue 			= new ReadyQueue();
		this.singleProcessor 		= new CPU();
		this.disk 					= new Device(deviceNames[0]);
		this.printer1 				= new Device(deviceNames[1]);
		this.printer2 				= new Device(deviceNames[2]);
		this.completed 				= new ArrayList <Process> ();
	}

	/**
	   Add new process to job pool
	   @param proc new process
	 */
	public void addJob(Process proc)
	{
		jobList.add(proc);
	}
	
	/**
	   Add process to Ready Queue
	   @param proc process added at arrival time
	 */
	public void addToReadyQueue(Process proc) 
	{
		this.readyQueue.add(proc);
	}
	
	/**
	   Returns the next process to CPU
	   @return next process to CPU
	 */
	public Process nextProcessToCPU() 
	{
		return readyQueue.nextProcessToCPU();
	}

	// Getter, Setter
	public ArrayList <Process> getCompletedList()
	{
		return this.completed;
	}
	public void setAging(boolean isAgingOn) 
	{
		aging = isAgingOn;
	}
	public boolean isAging()
	{
		return this.aging;
	}
	public void setTrace(boolean isTraceOn) 
	{
		trace = isTraceOn;
	}

	/**
	   Inserts process into Queue
	   @param proc Process to insert
	 */
	public void insertToQueue(Process proc) 
	{
		readyQueue.add(proc);
		traceStatement += "" + clockStatement +
			"Process " + proc.getID() + " Added to Queue " +
			readyQueue.currentQueueID(proc.getCurrentPriorityID()) + "\n";
	}

	/**
	   Sends a Process to a Device if
	    Process requested IO
	   @param proc IO requested Process
	 */
	public void insertToDevice(Process proc) {
		
		traceStatement += "" + clockStatement +
		"Process " + proc.getID() + " ";
		if (proc.getCurrentDevice().equals(deviceNames[0]))
		{
			disk.add(proc);
			traceStatement += "Moved to Disk Queue\n";
		}
		else if (proc.getCurrentDevice().equals(deviceNames[1]))
		{
			printer1.add(proc);
			traceStatement += "Moved to Printer 1 Queue\n";
		}
		else if (proc.getCurrentDevice().equals(deviceNames[2]))
		{
			printer2.add(proc);
			traceStatement += "Moved to Printer 2 Queue\n";
		}
		else 
			traceStatement += "Failed insert to device Queue";
		
	}

	/**
	   Update TurnAroundTime of the process, and
	   Inserts process into Completed Job list.
	   @param proc Completed Process
	 */
	public void insertToCompleted(Process proc) 
	{
		// update Process Time
		proc.setTurnAroundTime(clockTime);
		
		completed.add(proc);
		traceStatement += "" + clockStatement +
			"Process " + proc.getID() +
			" Completed\n";
		
	}
	
	/**
	   Performs Main Actions of the Scheduling
	    Algorithm as required for CS 149
	    Project Part 1 
	   @param tick unit increment time
	 */
	public void notify(double tick)
	{
		traceStatement = "";
		clockStatement = clockTime + "\t\t";
			
		// Check if new process arrives.
		Process nextProc = jobList.peekFirst();
		if (nextProc != null && nextProc.getArrivalTime() 
				== Simulator.getOneDecimal(clockTime))
		{
			traceStatement += "" + clockStatement +
				"Process " + nextProc.getID() + " Arrives to scheduler\n";
			insertToQueue(nextProc);
			jobList.remove();
		}
		
		// notify CPU of clock time increment
		Process fromCPU = singleProcessor.update(tick);
		if (fromCPU != null)
		{
			// Determine if Process returned from CPU requested IO
			if (hasRequestedIO(fromCPU))
			{
				traceStatement += "" + clockStatement + "Process " +
					fromCPU.getID() + " made a device request\n";
				insertToDevice(fromCPU);
				singleProcessor = new CPU(); // Releasing CPU
			}
			// Process returned from CPU completed
			//else if (Simulator.getOneDecimal(fromCPU.getCurrentRemainingTime()) <= 0)
			else if (fromCPU.getCurrentRemainingTime() <= 0)
			{
				insertToCompleted(fromCPU);
				singleProcessor = new CPU(); // Releasing CPU
			}
			// Process returned from CPU requires feedback
			else if(fromCPU.getFeedback())
			{
				traceStatement += "" + clockStatement + "Process " +
					fromCPU.getID() + " ends its CPU timeslice\n";

				// Do feedback.
				fromCPU.doFeedback(FEEDBACK);

				insertToQueue(fromCPU);
				// Releasing CPU
				singleProcessor = new CPU();
			}
			// Else CPU is still processing.
		}
		
		// notify Device(s) of clock time increment
		ArrayList <Process> fromDevices = new ArrayList <Process> ();
		Process pDisk = disk.update(tick);
		Process pP1   = printer1.update(tick);
		Process pP2   = printer2.update(tick);
		if (pDisk != null) 
			fromDevices.add(pDisk);
		if (pP1 != null)
			fromDevices.add(pP1);
		if (pP2 != null)
			fromDevices.add(pP2);
		
		for (int i = 0; i < fromDevices.size(); i++)
		{
			Process pFromDevice = fromDevices.get(i);
			
			traceStatement += "" + clockStatement
				+ pFromDevice.getCurrentDevice() + 
				" Operation terminated for process "
				+ pFromDevice.getID() + "\n";
			//pFromDevice.resetDeviceLifetime();
			if (pFromDevice.getCurrentRemainingTime() > 0)
				insertToQueue(pFromDevice);
			else
				insertToCompleted(pFromDevice);
		}
		
		// Check if a Process needs to utilize the CPU
		//  if true, send Process to CPU
		
		
		Process dispatch = nextProcessToCPU();
		// Check if current Process in CPU must be
		// pre-empted out for a new Process (if any)
		if (dispatch != null && singleProcessor.isReady(dispatch))
		{
			if (!singleProcessor.isEmpty())
			{
				Process preempted = singleProcessor.preempt();
				preempted.setCurrentTimeSlice
					(preempted.getCurrentQueueNumber());
				insertToQueue(preempted);
				singleProcessor = new CPU();
			}
			dispatch.setCurrentDevice("CPU");
			dispatch.setResponseTime(clockTime);
			singleProcessor.put(dispatch);
			readyQueue.remove(dispatch);
			traceStatement += "" + clockStatement +
				"Process " + dispatch.getID() + 
				" Added to CPU process\n";
		}
		
		// Updates wait time.
		readyQueue.updateProcessWaitTimes(tick);
		
		// Prints Trace to STDOUT.
		if (trace)
			System.out.print(traceStatement);
		
		clockTime += tick;
		clockTime = Double.parseDouble(df.format((clockTime)));
		
		// check for Aging prevention
		if (Simulator.getOneDecimal(clockTime % AGING) == 0 
				&& clockTime != 0)
			readyQueue.preventAging(trace);
	}
	
	/**
	   Determines whether a Process has requested IO
	    If a process requests IO, Process current device
	    name will be set appropriately
	    If a process requests both Disk & Printer at the 
	    same time, Disk will not be considered
	   @param proc Process to be considered for IO request
	   @return true if requested, else false
	 */
	private boolean hasRequestedIO(Process proc) 
	{
		boolean requested = false;
		if (proc.getDiskLifeTime() > 0 && 
				Math.random() <= proc.getProbDiskRequest())
		{
			proc.setCurrentDevice(deviceNames[0]);
			requested = true;
		}
		if (proc.getPr1LifeTime() > 0 &&
				Math.random() <= proc.getProbPr1Request())
		{
			proc.setCurrentDevice(deviceNames[1]);
			requested = true;
		}
		if (proc.getPr2LifeTime() > 0 &&
				Math.random() <= proc.getProbPr2Request())
		{
			proc.setCurrentDevice(deviceNames[2]);
			requested = true;
		}
		return requested;
	}
	
	/**
	   Returns a description of the current status
	    of the Queues 
	   @return updated simulation queue log
	 */
	public String displayQueues()
	{
		String status = readyQueue.toString();
		status       += disk.toString();
		status       += printer1.toString();
		status       += printer2.toString();
		return status;
	}
}
